#!/bin/sh

# parse options
while [ -n "$1" ]; do
	case "$1" in
		--version|-v) export VERSION=1; break;;
		--url) export URL=${2%/}; shift;;  # strip trailing slash
		--interval) export INTERVAL=$2; shift;;
		--verify-ssl) export VERIFY_SSL=$2; shift;;
		--uuid) export UUID="$2"; shift;;
		--key) export KEY="$2"; shift;;
		--shared-secret) export SHARED_SECRET="$2"; shift;;
		--consistent-key) export CONSISTENT_KEY="$2"; shift;;
		--hardware-id-script) export HARDWARE_ID_SCRIPT="$2"; shift;;
		--hardware-id-key) export HARDWARE_ID_KEY="$2"; shift;;
		--bootup-delay) export BOOTUP_DELAY="$2"; shift;;
		--merge-config) export MERGE_CONFIG="$2"; shift;;
		--unmanaged) export UNMANAGED="$2"; shift;;
		--test-config) export TEST_CONFIG="$2"; shift;;
		--test-script) export TEST_SCRIPT="$2"; shift;;
		--connect-timeout) export CONNECT_TIMEOUT="$2"; shift;;
		--max-time) export MAX_TIME="$2"; shift;;
		--capath) export CAPATH="$2"; shift;;
		--cacert) export CACERT="$2"; shift;;
		--mac-interface) export MAC_INTERFACE="$2"; shift;;
		--management-interface) export MANAGEMENT_INTERFACE="$2"; shift;;
		--pre-reload-hook) export PRE_RELOAD_HOOK="$2"; shift;;
		--post-reload-hook) export POST_RELOAD_HOOK="$2"; shift;;
		--post-registration-hook) export POST_REGISTRATION_HOOK="$2"; shift;;
		-*)
			echo "Invalid option: $1"
			exit 1
		;;
		*) break;;
	esac
	shift;
done

if [ "$VERSION" -eq "1" ]; then
	VERSION=$(cat /etc/openwisp/VERSION)
	echo "openwisp-config $VERSION"
	exit 0
fi

if [ -z "$URL" ]; then
	logger -s "missing required --url option" \
		   -t openwisp \
		   -p daemon.err
	exit 2
fi

if ([ -z "$UUID" ] || [ -z "$KEY" ]) && [ -z "$SHARED_SECRET" ]; then
	logger -s "you must either specify --uuid and --key, or --shared-secret" \
		   -t openwisp \
		   -p daemon.err
	exit 3
fi

INTERVAL=${INTERVAL:-120}
VERIFY_SSL=${VERIFY_SSL:-1}
MERGE_CONFIG=${MERGE_CONFIG:-1}
TEST_CONFIG=${TEST_CONFIG:-1}
CONSISTENT_KEY=${CONSISTENT_KEY:-1}
HARDWARE_ID_KEY=${HARDWARE_ID_KEY:-1}
BOOTUP_DELAY=${BOOTUP_DELAY:-0}
CONNECT_TIMEOUT=${CONNECT_TIMEOUT:-15}
MAX_TIME=${MAX_TIME:-30}
MAC_INTERFACE=${MAC_INTERFACE:-eth0}
PRE_RELOAD_HOOK=${PRE_RELOAD_HOOK:-/etc/openwisp/pre-reload-hook}
POST_RELOAD_HOOK=${POST_RELOAD_HOOK:-/etc/openwisp/post-reload-hook}
POST_REGISTRATION_HOOK=${POST_REGISTRATION_HOOK:-/etc/openwisp/post-registration-hook}
WORKING_DIR="/tmp/openwisp"
BASEURL="$URL/controller"
CONFIGURATION_ARCHIVE="$WORKING_DIR/configuration.tar.gz"
CONFIGURATION_CHECKSUM="$WORKING_DIR/checksum"
CONFIGURATION_BACKUP="$WORKING_DIR/backup.tar.gz"
REGISTRATION_PARAMETERS="$WORKING_DIR/registration_parameters"
TEST_CHECKSUM="$WORKING_DIR/test_checksum"
UPDATE_INFO="$WORKING_DIR/update_info"
STATUS_REPORT="$WORKING_DIR/status_report"
APPLYING_CONF="$WORKING_DIR/applying_conf"
BOOTUP="$WORKING_DIR/bootup"
REGISTRATION_URL="$URL/controller/register/"
UNMANAGED_DIR="$WORKING_DIR/unmanaged"
FETCH_COMMAND="curl -s --connect-timeout $CONNECT_TIMEOUT --max-time $MAX_TIME"
mkdir -p $WORKING_DIR
mkdir -p $UNMANAGED_DIR

if [ "$VERIFY_SSL" == "0" ]; then
	FETCH_COMMAND="$FETCH_COMMAND -k"
else
	if [ -n "$CAPATH" ]; then
		FETCH_COMMAND="$FETCH_COMMAND --capath $CAPATH"
	fi
	if [ -n "$CACERT" ]; then
		FETCH_COMMAND="$FETCH_COMMAND --cacert $CACERT"
	fi
fi

if [ ! -x "$HARDWARE_ID_SCRIPT" ]; then
	HARDWARE_ID_KEY=0
fi

if [ -n "$UNMANAGED" ]; then
	# replace commas with spaces
	UNMANAGED=$(echo $UNMANAGED | tr ',' ' ')
fi

# if management interface is not ready, the ip will be empty but other
# attempts at determining it will be done before the start-up is completed
if [ -n "$MANAGEMENT_INTERFACE" ]; then
	MANAGEMENT_IP=$(/usr/sbin/openwisp-get-address $MANAGEMENT_INTERFACE)
fi

get_model(){ cat /tmp/sysinfo/model; }
get_os(){ ubus call system board | grep description | awk -F\" '{ print $4 }'; }
get_system(){ ubus call system board | grep system | awk -F\" '{ print $4 }'; }

# ensures we are dealing with the right web server
check_header(){
	local is_controller=$(grep -c "X-Openwisp-Controller: true" $1)
	if [ $is_controller -lt 1 ]; then
		logger -s "Invalid url: missing X-Openwisp-Controller header" \
			   -t openwisp \
			   -p daemon.err
		exit 4
	fi
}

# performs automatic registration
register() {
	logger "Registering device..." \
		   -t openwisp \
		   -p daemon.info
	local hostname=$(uci get system.@system[0].hostname)
	# use macaddr of interface specified in $MAC_INTERFACE
	local raw=$(ifconfig $MAC_INTERFACE 2&> /dev/null)
	# if previous command fails, fallback to first non-loopback interface
	if [ -z "$raw" ]; then raw=$(ifconfig); fi
	local macaddr=$(echo "$raw " | egrep -v "^(lo|tap[0-9]+)" | awk '/HWaddr/ { print $5 }' | head -n 1)
	# convert hostname to lowercase for case insensitive check
	local name=$(echo $hostname | awk '{print tolower($0)}')
	# use macaddress if hostname is the default one or empty
	if [ "$name" == "openwrt" ] || [ "$name" == "lede" ] || [ -z "$name" ]; then
		hostname="$macaddr"
	fi
	local backend="netjsonconfig.OpenWrt"
	local tags=$(uci get openwisp.http.tags 2&> /dev/null)
	# get device model, OS identifier and SOC info
	local model=$(get_model)
	local os=$(get_os)
	local system=$(get_system)
	# get hardware id if script is given
	if [ -x "$HARDWARE_ID_SCRIPT" ]; then
		local hardware_id=$("$HARDWARE_ID_SCRIPT")
	else
		local hardware_id=""
	fi
	# prepare POST params
	local params="backend=$backend"
	if [ "$CONSISTENT_KEY" == "1" ]; then
		if [ -x "$HARDWARE_ID_SCRIPT" ] && [ "$HARDWARE_ID_KEY" = "1" ]; then
			# use hardware id as base for the key
			local keybase=$hardware_id
		else
			# use macaddress as base for the key
			local keybase=$macaddr
		fi
		local consistent_key=$(echo -n "$keybase+$SHARED_SECRET" | md5sum | awk '{print $1}')
		params="$params&key=$consistent_key"
	fi
	# add management ip in params if present
	if [ -n "$MANAGEMENT_IP" ]; then
		params="$params&management_ip=$MANAGEMENT_IP"
	fi
	$($FETCH_COMMAND --data $params \
	                 --data-urlencode secret="$SHARED_SECRET" \
	                 --data-urlencode name="$hostname" \
	                 --data-urlencode hardware_id="$hardware_id" \
	                 --data-urlencode mac_address="$macaddr" \
	                 --data-urlencode tags="$tags" \
	                 --data-urlencode model="$model" \
	                 --data-urlencode os="$os" \
	                 --data-urlencode system="$system" \
	                 -i $REGISTRATION_URL > $REGISTRATION_PARAMETERS)
	local exit_code=$?
	# report eventual failures and return
	if [ "$exit_code" != "0" ]; then
		logger -s "Failed to connect to controller during registration: curl exit code $exit_code" \
		       -t openwisp \
		       -p daemon.err
		return 1
	fi
	# exit if response does not seem to come from openwisp controller
	check_header $REGISTRATION_PARAMETERS
	if [ $(head -n 1 $REGISTRATION_PARAMETERS | grep -c "201 Created") -lt 1 ]; then
		local message=$(tail -n 1 $REGISTRATION_PARAMETERS)
		logger -s "Registration failed! $message" \
			   -t openwisp \
			   -p daemon.err
		# if controller returns 403, stop retrying because it's useless
		if [ $(head -n 1 $REGISTRATION_PARAMETERS | grep -c "403 Forbidden") -gt 0 ]; then
			exit 5
		# in all other cases, keep re-trying because the problem can be fixed
		# on the server (eg: self creation disabled and device needs to be created
		# or temporary error condition on the server)
		else
			return 2
		fi
	fi
	# set configuration options and reload
	export UUID=$(cat $REGISTRATION_PARAMETERS | grep uuid | awk '/uuid: / { print $2 }')
	export KEY=$(cat $REGISTRATION_PARAMETERS | grep key | awk '/key: / { print $2 }')
	local name=$(cat $REGISTRATION_PARAMETERS | grep hostname | awk '/hostname: / { print $2 }')
	local new=$(cat $REGISTRATION_PARAMETERS | grep 'is-new' | awk '/is-new: / { print $2 }')
	# keep backward compatibility with older controller (TODO: remove this in 0.5)
	[ -z "$name" ] && name="$hostname"
	[ -z "$new" ] && new="1"
	# persist uuid and key in conf
	uci set openwisp.http.uuid=$UUID
	uci set openwisp.http.key=$KEY
	# remove shared secret to avoid accidental re-registration
	uci set openwisp.http.shared_secret=""
	uci commit openwisp
	rm $REGISTRATION_PARAMETERS
	# log operation
	local prefix=$([ "$new" == "1" ] && echo "New" || echo "Existing")
	logger "$prefix device registered successfully as $name, id: $UUID" \
		   -t openwisp \
		   -p daemon.info
	# indicates this function has been called
	# (used by `discover_management_ip`)
	export REGISTER_CALLED=1
	# call post registration hook
	post_registration_hook
}

# gets checksum from controller
get_checksum() {
	$($FETCH_COMMAND -i $CHECKSUM_URL > $1)
	local exit_code=$?

	if [ "$exit_code" != "0" ]; then
		logger -s "Failed to connect to controller while getting checksum: curl exit code $exit_code" \
		       -t openwisp \
		       -p daemon.err
		return 2
	fi

	if [ $(head -n 1 $1 | grep -c "200 OK") -lt 1 ]; then
		local status=$(head -n 1 $1)
		logger -s "Failed to retrieve checksum: $status" \
			   -t openwisp \
			   -p daemon.err
		return 3
	fi
	check_header $1
}

# returns 1 if configuration in controller has changed
configuration_changed() {
	local CURRENT_CHECKSUM=$(tail -n 1 $CONFIGURATION_CHECKSUM 2> /dev/null)
	get_checksum $CONFIGURATION_CHECKSUM
	local exit_code=$?

	if [ "$exit_code" != "0" ]; then
		return $exit_code
	fi

	local REMOTE_CHECKSUM=$(tail -n 1 $CONFIGURATION_CHECKSUM 2> /dev/null)

	if [ "$CURRENT_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then
		logger "Local configuration outdated" \
		       -t openwisp \
		       -p daemon.info
		return 1
	fi

	return 0
}

# called in `apply_configuration` before services are reloaded
pre_reload_hook() {
	if [ -x "$PRE_RELOAD_HOOK" ]; then
		$PRE_RELOAD_HOOK
		local exit_code=$?
		logger "Called pre-reload-hook: $PRE_RELOAD_HOOK - exit code: $exit_code" \
		       -t openwisp \
		       -p daemon.info
		return $exit_code
	fi
}

# called in `apply_configuration` after services are reloaded
post_reload_hook() {
	if [ -x "$POST_RELOAD_HOOK" ]; then
		$POST_RELOAD_HOOK
		local exit_code=$?
		logger "Called post-reload-hook: $POST_RELOAD_HOOK - exit code: $exit_code" \
		       -t openwisp \
		       -p daemon.info
		return $exit_code
	fi
}

# called after registration is completed
post_registration_hook() {
	if [ -x "$POST_REGISTRATION_HOOK" ]; then
		$POST_REGISTRATION_HOOK
		local exit_code=$?
		logger "Called post-reload-hook: $POST_REGISTRATION_HOOK - exit code: $exit_code" \
		       -t openwisp \
		       -p daemon.info
		return $exit_code
	fi
}

# applies a specified configuration archive
apply_configuration() {
	local sleep_time=${2-5}
	# store unmanaged config (if enabled)
	call_store_unmanaged
	# update local configuration
	/usr/sbin/openwisp-update-config --merge=$MERGE_CONFIG
	local exit_code=$?
	if [ "$exit_code" != "0" ]; then
		logger -s "Could not update configuration, openwisp-update-config exit code was $exit_code" \
		       -t openwisp \
		       -p daemon.crit
		return 1
	fi
	# restore unmanaged configurations
	call_restore_unmanaged
	# call pre-reload-hook
	pre_reload_hook
	# reload changes and wait $sleep_time
	/usr/sbin/openwisp-reload-config
	sleep $sleep_time
	# call post-reload-hook
	post_reload_hook
}

# send up to date device info to the controller
update_info() {
	# get device model, OS identifier and SOC info
	local model=$(get_model)
	local os=$(get_os)
	local system=$(get_system)
	# retry several times
	for i in $(seq 1 10); do
		$FETCH_COMMAND -i \
			 --data "key=$KEY" \
			 --data-urlencode model="$model" \
			 --data-urlencode os="$os" \
			 --data-urlencode system="$system" \
		"$UPDATE_INFO_URL" > $UPDATE_INFO
		local exit_code=$?
		if [ "$exit_code" = "0" ]; then
			logger "Device info updated on the controller" \
			       -t openwisp \
			       -p daemon.info
			break
		else
			sleep 2
		fi
	done

	if [ "$exit_code" != "0" ]; then
		logger -s "Failed to connect to controller during update-info: curl exit code $exit_code" \
		       -t openwisp \
		       -p daemon.err
		return 2
	fi

	if [ "$(head -n 1 "$UPDATE_INFO" | grep -c "200 OK")" -lt 1 ]; then
		local status=$(head -n 1 $UPDATE_INFO)
		logger -s "Failed to update device info: $status" \
		       -t openwisp \
		       -p daemon.err
		return 3
	fi
	check_header $UPDATE_INFO
	rm $UPDATE_INFO
}

# report configuration status: "running" or "error"
report_status() {
	# retry several times
	for i in $(seq 1 10); do
		$($FETCH_COMMAND -i --data "key=$KEY&status=$1" $REPORT_URL > $STATUS_REPORT)
		local exit_code=$?
		if [ "$exit_code" == "0" ]; then
			break
		else
			sleep 2
		fi
	done

	if [ "$exit_code" != "0" ]; then
		logger -s "Failed to connect to controller during report-status: curl exit code $exit_code" \
		       -t openwisp \
		       -p daemon.err
		return 2
	fi

	if [ $(head -n 1 $STATUS_REPORT | grep -c "200 OK") -lt 1 ]; then
		local status=$(head -n 1 $STATUS_REPORT)
		logger -s "Failed to report status: $status" \
		       -t openwisp \
		       -p daemon.err
		return 3
	fi
	check_header $STATUS_REPORT
	rm $STATUS_REPORT
}

# performs configuration test and reports result
test_configuration() {
	sysupgrade -b $CONFIGURATION_BACKUP
	apply_configuration $1
	if [ $? -gt 0 ]; then
		logger -s "Configuration could not be applied, update operation aborted" \
		       -t openwisp \
		       -p daemon.err
		report_status "error"
		# if this case is triggered, the configuration may
		# be damaged and so we need to do some clean up
		sysupgrade -r $CONFIGURATION_BACKUP
		logger -s "The most recent configuration backup was restored" \
		       -t openwisp \
		       -p daemon.info
		return 1
	fi

	logger "Testing configuration..." \
	       -t openwisp \
	       -p daemon.info

	if [ -z "$TEST_SCRIPT" ]; then
		perform_default_test
		local test_result=$?
	else
		$TEST_SCRIPT
		local test_result=$?
	fi

	if [ $test_result -gt 0 ]; then
		logger -s "Configuration test failed! Restoring previous backup" \
		       -t openwisp \
		       -p daemon.err
		apply_configuration $CONFIGURATION_BACKUP
		report_status "error"
		local ret=1
	else
		logger "Configuration test succeeded" \
		       -t openwisp \
		       -p daemon.info
		report_status "running"
		local ret=0
	fi

	rm $CONFIGURATION_BACKUP
	return $ret
}

perform_default_test() {
	# max 3 attempts to get checksum
	for i in $(seq 1 3); do
		$($FETCH_COMMAND -i --connect-timeout 5 --max-time 5 $CHECKSUM_URL > $TEST_CHECKSUM)
		local result=$?
		if [ $result -gt 0 ]; then
			sleep 5
		else
			break
		fi
	done
	rm $TEST_CHECKSUM
	return $result
}

# stores unmanaged configuration sections that will be merged
# with the configuration downloaded from the controller
call_store_unmanaged() {
	if [ -z "$UNMANAGED" ]; then
		return 0
	fi
	/usr/sbin/openwisp-store-unmanaged -o="$UNMANAGED"
}

# restores unmanaged configuration sections that will be merged
# with the configuration downloaded from the controller
call_restore_unmanaged() {
	if [ -z "$UNMANAGED" ]; then
		return 0
	fi
	/usr/sbin/openwisp-restore-unmanaged
}

# 1. removes default wifi-ifaces directive (LEDE or OpenWrt SSID)
# 2. ensures there are no anonymous UCI blocks
fix_uci_config() {
	/usr/sbin/openwisp-remove-default-wifi
	output=$(/usr/sbin/openwisp-uci-autoname)
	if [ -n "$output" ]; then
		logger "The following uci configs have been renamed: $output" \
		       -t openwisp \
		       -p daemon.info
	fi
}

# downloads configuration from controller
# performs test (if testing enabled)
# and applies it
update_configuration() {
	logger "Downloading configuration from controller..." \
		   -t openwisp \
		   -p daemon.info

	# download configuration
	$($FETCH_COMMAND $CONFIGURATION_URL -o $CONFIGURATION_ARCHIVE)
	local exit_code=$?

	if [ "$exit_code" != "0" ]; then
		logger -s "Failed to connect to controller while downloading new config: curl exit code $exit_code" \
		       -t openwisp \
		       -p daemon.err
		return 3
	fi

	logger "Configuration downloaded, now applying it..." \
		   -t openwisp \
		   -p daemon.info

	# makes fixes to the default uci config so
	# it's more suited for remote control
	fix_uci_config

	# control file to avoid reloading the agent while
	# configuration is still being applied
	touch $APPLYING_CONF

	# testing enabled
	if [ "$TEST_CONFIG" == "1" ]; then
		test_configuration $CONFIGURATION_ARCHIVE
		result=$?
	# testing diabled
	else
		apply_configuration $CONFIGURATION_ARCHIVE
		# only try to report success
		report_status "running"
		result=$?
	fi

	rm $APPLYING_CONF

	if [ "$result" == "0" ]; then
		logger "Configuration applied successfully" \
		       -t openwisp \
		       -p daemon.info
	fi
}

discover_management_ip(){
	# return if not using management interface
	if [ -z "$MANAGEMENT_INTERFACE" ]; then
		return
	fi
	# if being called just after `register` was called
	if [ -n "$REGISTER_CALLED" ]; then
		# don't do anything now, but unset the
		# control variable so the logic will be
		# executed at the next iteration
		unset REGISTER_CALLED
		return
	fi
	MANAGEMENT_IP=$(/usr/sbin/openwisp-get-address $MANAGEMENT_INTERFACE)
	# if management interface is defined but its ip could not be determined
	# try waiting for the management interface to be ready
	if [ -z "$MANAGEMENT_IP" ]; then
		MAX_ATTEMPTS=3
		for attempt in `seq $MAX_ATTEMPTS`; do
			MANAGEMENT_IP=$(/usr/sbin/openwisp-get-address $MANAGEMENT_INTERFACE)

			if [ -z "$MANAGEMENT_IP" ]; then
				retry_after=$(expr $INTERVAL / 12)
				logger -s "management interface not ready (attempt $attempt of $MAX_ATTEMPTS)" \
				       -t openwisp \
				       -p daemon.notice
				if [ $attempt -lt $MAX_ATTEMPTS ]; then
					sleep $retry_after
				else
					logger -s "could not determine ip address of management interface, giving up..." \
					       -t openwisp \
					       -p daemon.warning
				fi
			else
				break
			fi
		done
	fi
	# add management ip to URLs if present
	if [ -n "$MANAGEMENT_IP" ]; then
		export CONFIGURATION_URL="$BASE_CONFIGURATION_URL&management_ip=$MANAGEMENT_IP"
		export CHECKSUM_URL="$BASE_CHECKSUM_URL&management_ip=$MANAGEMENT_IP"
	fi
}

# ensure both UUID and KEY are defined
# otherwise perform registration
if [ -z "$UUID" ] || [ -z "$KEY" ]; then
	# do not crash if controller can't be reached
	# but retry every ($INTERVAL / 4) seconds
	# (device might be unplugged, unconfigured or connecting)
	until register
	do
		sleep $(expr $INTERVAL / 4)
	done
	touch "$BOOTUP"
fi

# these variables are evaluated here because "register()" might set UUID and KEY
BASE_CONFIGURATION_URL="$BASEURL/download-config/$UUID/?key=$KEY"
BASE_CHECKSUM_URL="$BASEURL/checksum/$UUID/?key=$KEY"
CONFIGURATION_URL=$BASE_CONFIGURATION_URL
CHECKSUM_URL=$BASE_CHECKSUM_URL
UPDATE_INFO_URL="$BASEURL/update-info/$UUID/"
REPORT_URL="$BASEURL/report-status/$UUID/"

if [ ! -f "$BOOTUP" ]; then
	# actions to do only after agent start, not after registration or agent restart
	if [ "$BOOTUP_DELAY" != "0" ]; then
		# random bootup delay between 0..300 seconds
		DELAY="$(($RANDOM%$BOOTUP_DELAY))"
		logger "Delaying startup for $DELAY seconds..." \
				 -t openwisp \
				 -p daemon.info
		sleep $DELAY
	fi
	# send up to date device info
	update_info
	touch "$BOOTUP"
fi

while true
do
	# check management interface at each iteration
	# (because the address may change due to
	# configuration updates or manual reconfigurations)
	discover_management_ip

	configuration_changed

	if [ "$?" == "1" ]; then
		update_configuration
	fi

	sleep $INTERVAL
done
